home *** CD-ROM | disk | FTP | other *** search
- // Copyright 2014 The Chromium Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
-
- cr.define('cr.ui.pageManager', function() {
- /** @const */ var FocusOutlineManager = cr.ui.FocusOutlineManager;
-
- /**
- * PageManager contains a list of root Page and overlay Page objects and
- * handles "navigation" by showing and hiding these pages and overlays. On
- * initial load, PageManager can use the path to open the correct hierarchy
- * of pages and overlay(s). Handlers for user events, like pressing buttons,
- * can call into PageManager to open a particular overlay or cancel an
- * existing overlay.
- */
- var PageManager = {
- /**
- * True if page is served from a dialog.
- * @type {boolean}
- */
- isDialog: false,
-
- /**
- * Offset of page container in pixels. Uber pages that use the side menu
- * can override this with the setter.
- * The default (23) comes from -webkit-margin-start in uber_shared.css.
- * @type {number}
- */
- horizontalOffset_: 23,
-
- /**
- * Root pages. Maps lower-case page names to the respective page object.
- * @type {!Object.<string, !cr.ui.pageManager.Page>}
- */
- registeredPages: {},
-
- /**
- * Pages which are meant to behave like modal dialogs. Maps lower-case
- * overlay names to the respective overlay object.
- * @type {!Object.<string, !cr.ui.pageManager.Page>}
- * @private
- */
- registeredOverlayPages: {},
-
- /**
- * Observers will be notified when opening and closing overlays.
- * @type {!Array.<!cr.ui.pageManager.PageManager.Observer>}
- */
- observers_: [],
-
- /**
- * Initializes the complete page.
- * @param {cr.ui.pageManager.Page} defaultPage The page to be shown when no
- * page is specified in the path.
- */
- initialize: function(defaultPage) {
- this.defaultPage_ = defaultPage;
-
- FocusOutlineManager.forDocument(document);
- document.addEventListener('scroll', this.handleScroll_.bind(this));
-
- // Trigger the scroll handler manually to set the initial state.
- this.handleScroll_();
-
- // Shake the dialog if the user clicks outside the dialog bounds.
- var containers = document.querySelectorAll('body > .overlay');
- for (var i = 0; i < containers.length; i++) {
- var overlay = containers[i];
- cr.ui.overlay.setupOverlay(overlay);
- overlay.addEventListener('cancelOverlay',
- this.cancelOverlay.bind(this));
- }
-
- cr.ui.overlay.globalInitialization();
- },
-
- /**
- * Registers new page.
- * @param {!cr.ui.pageManager.Page} page Page to register.
- */
- register: function(page) {
- this.registeredPages[page.name.toLowerCase()] = page;
- page.initializePage();
- },
-
- /**
- * Registers a new Overlay page.
- * @param {!cr.ui.pageManager.Page} overlay Overlay to register.
- * @param {cr.ui.pageManager.Page} parentPage Associated parent page for
- * this overlay.
- * @param {Array} associatedControls Array of control elements associated
- * with this page.
- */
- registerOverlay: function(overlay,
- parentPage,
- associatedControls) {
- this.registeredOverlayPages[overlay.name.toLowerCase()] = overlay;
- overlay.parentPage = parentPage;
- if (associatedControls) {
- overlay.associatedControls = associatedControls;
- if (associatedControls.length) {
- overlay.associatedSection =
- this.findSectionForNode_(associatedControls[0]);
- }
-
- // Sanity check.
- for (var i = 0; i < associatedControls.length; ++i) {
- assert(associatedControls[i], 'Invalid element passed.');
- }
- }
-
- overlay.tab = undefined;
- overlay.isOverlay = true;
-
- overlay.reverseButtonStrip();
- overlay.initializePage();
- },
-
- /**
- * Shows the default page.
- * @param {boolean=} opt_updateHistory If we should update the history after
- * showing the page (defaults to true).
- */
- showDefaultPage: function(opt_updateHistory) {
- assert(this.defaultPage_ instanceof cr.ui.pageManager.Page,
- 'PageManager must be initialized with a default page.');
- this.showPageByName(this.defaultPage_.name, opt_updateHistory);
- },
-
- /**
- * Shows a registered page. This handles both root and overlay pages.
- * @param {string} pageName Page name.
- * @param {boolean=} opt_updateHistory If we should update the history after
- * showing the page (defaults to true).
- * @param {Object=} opt_propertyBag An optional bag of properties including
- * replaceState (if history state should be replaced instead of pushed).
- * hash (a hash state to attach to the page).
- */
- showPageByName: function(pageName,
- opt_updateHistory,
- opt_propertyBag) {
- opt_updateHistory = opt_updateHistory !== false;
- opt_propertyBag = opt_propertyBag || {};
-
- // If a bubble is currently being shown, hide it.
- this.hideBubble();
-
- // Find the currently visible root-level page.
- var rootPage = null;
- for (var name in this.registeredPages) {
- var page = this.registeredPages[name];
- if (page.visible && !page.parentPage) {
- rootPage = page;
- break;
- }
- }
-
- // Find the target page.
- var targetPage = this.registeredPages[pageName.toLowerCase()];
- if (!targetPage || !targetPage.canShowPage()) {
- // If it's not a page, try it as an overlay.
- var hash = opt_propertyBag.hash || '';
- if (!targetPage && this.showOverlay_(pageName, hash, rootPage)) {
- if (opt_updateHistory)
- this.updateHistoryState_(!!opt_propertyBag.replaceState);
- this.updateTitle_();
- return;
- }
- targetPage = this.defaultPage_;
- }
-
- pageName = targetPage.name.toLowerCase();
- var targetPageWasVisible = targetPage.visible;
-
- // Determine if the root page is 'sticky', meaning that it
- // shouldn't change when showing an overlay. This can happen for special
- // pages like Search.
- var isRootPageLocked =
- rootPage && rootPage.sticky && targetPage.parentPage;
-
- // Notify pages if they will be hidden.
- this.forEachPage_(!isRootPageLocked, function(page) {
- if (page.name != pageName && !this.isAncestorOfPage(page, targetPage))
- page.willHidePage();
- });
-
- // Update the page's hash.
- targetPage.hash = opt_propertyBag.hash || '';
-
- // Update visibilities to show only the hierarchy of the target page.
- this.forEachPage_(!isRootPageLocked, function(page) {
- page.visible = page.name == pageName ||
- this.isAncestorOfPage(page, targetPage);
- });
-
- // Update the history and current location.
- if (opt_updateHistory)
- this.updateHistoryState_(!!opt_propertyBag.replaceState);
-
- // Update focus if any other control was focused on the previous page,
- // or the previous page is not known.
- if (document.activeElement != document.body &&
- (!rootPage || rootPage.pageDiv.contains(document.activeElement))) {
- targetPage.focus();
- }
-
- // Notify pages if they were shown.
- this.forEachPage_(!isRootPageLocked, function(page) {
- if (!targetPageWasVisible &&
- (page.name == pageName ||
- this.isAncestorOfPage(page, targetPage))) {
- page.didShowPage();
- }
- });
-
- // If the target page was already visible, notify it that its hash
- // changed externally.
- if (targetPageWasVisible)
- targetPage.didChangeHash();
-
- // Update the document title. Do this after didShowPage was called, in
- // case a page decides to change its title.
- this.updateTitle_();
- },
-
- /**
- * Returns the name of the page from the current path.
- * @return {string} Name of the page specified by the current path.
- */
- getPageNameFromPath: function() {
- var path = location.pathname;
- if (path.length <= 1)
- return this.defaultPage_.name;
-
- // Skip starting slash and remove trailing slash (if any).
- return path.slice(1).replace(/\/$/, '');
- },
-
- /**
- * Gets the level of the page. Root pages (e.g., BrowserOptions) are at
- * level 0.
- * @return {number} How far down this page is from the root page.
- */
- getNestingLevel: function(page) {
- var level = 0;
- var parent = page.parentPage;
- while (parent) {
- level++;
- parent = parent.parentPage;
- }
- return level;
- },
-
- /**
- * Checks whether one page is an ancestor of the other page in terms of
- * subpage nesting.
- * @param {cr.ui.pageManager.Page} potentialAncestor Potential ancestor.
- * @param {cr.ui.pageManager.Page} potentialDescendent Potential descendent.
- * @return {boolean} True if |potentialDescendent| is nested under
- * |potentialAncestor|.
- */
- isAncestorOfPage: function(potentialAncestor, potentialDescendent) {
- var parent = potentialDescendent.parentPage;
- while (parent) {
- if (parent == potentialAncestor)
- return true;
- parent = parent.parentPage;
- }
- return false;
- },
-
- /**
- * Returns true if the page is a direct descendent of a root page, or if
- * the page is considered always on top. Doesn't consider visibility.
- * @param {cr.ui.pageManager.Page} page Page to check.
- * @return {boolean} True if |page| is a top-level overlay.
- */
- isTopLevelOverlay: function(page) {
- return page.isOverlay &&
- (page.alwaysOnTop || this.getNestingLevel(page) == 1);
- },
-
- /**
- * Called when an page is shown or hidden to update the root page
- * based on the page's new visibility.
- * @param {cr.ui.pageManager.Page} page The page being made visible or
- * invisible.
- */
- onPageVisibilityChanged: function(page) {
- this.updateRootPageFreezeState();
-
- for (var i = 0; i < this.observers_.length; ++i)
- this.observers_[i].onPageVisibilityChanged(page);
-
- if (!page.visible && this.isTopLevelOverlay(page))
- this.updateScrollPosition_();
- },
-
- /**
- * Called when a page's hash changes. If the page is the topmost visible
- * page, the history state is updated.
- * @param {cr.ui.pageManager.Page} page The page whose hash has changed.
- */
- onPageHashChanged: function(page) {
- if (page == this.getTopmostVisiblePage())
- this.updateHistoryState_(false);
- },
-
- /**
- * Returns the topmost visible page, or null if no page is visible.
- * @return {cr.ui.pageManager.Page} The topmost visible page.
- */
- getTopmostVisiblePage: function() {
- // Check overlays first since they're top-most if visible.
- return this.getVisibleOverlay_() ||
- this.getTopmostVisibleNonOverlayPage_();
- },
-
- /**
- * Closes the visible overlay. Updates the history state after closing the
- * overlay.
- */
- closeOverlay: function() {
- var overlay = this.getVisibleOverlay_();
- if (!overlay)
- return;
-
- overlay.visible = false;
- overlay.didClosePage();
-
- this.updateHistoryState_(false);
- this.updateTitle_();
-
- this.restoreLastFocusedElement_();
- },
-
- /**
- * Closes all overlays and updates the history after each closed overlay.
- */
- closeAllOverlays: function() {
- while (this.isOverlayVisible_()) {
- this.closeOverlay();
- }
- },
-
- /**
- * Cancels (closes) the overlay, due to the user pressing <Esc>.
- */
- cancelOverlay: function() {
- // Blur the active element to ensure any changed pref value is saved.
- document.activeElement.blur();
- var overlay = this.getVisibleOverlay_();
- if (!overlay)
- return;
- // Let the overlay handle the <Esc> if it wants to.
- if (overlay.handleCancel) {
- overlay.handleCancel();
- this.restoreLastFocusedElement_();
- } else {
- this.closeOverlay();
- }
- },
-
- /**
- * Shows an informational bubble displaying |content| and pointing at the
- * |target| element. If |content| has focusable elements, they join the
- * current page's tab order as siblings of |domSibling|.
- * @param {HTMLDivElement} content The content of the bubble.
- * @param {HTMLElement} target The element at which the bubble points.
- * @param {HTMLElement} domSibling The element after which the bubble is
- * added to the DOM.
- * @param {cr.ui.ArrowLocation} location The arrow location.
- */
- showBubble: function(content, target, domSibling, location) {
- this.hideBubble();
-
- var bubble = new cr.ui.AutoCloseBubble;
- bubble.anchorNode = target;
- bubble.domSibling = domSibling;
- bubble.arrowLocation = location;
- bubble.content = content;
- bubble.show();
- this.bubble_ = bubble;
- },
-
- /**
- * Hides the currently visible bubble, if any.
- */
- hideBubble: function() {
- if (this.bubble_)
- this.bubble_.hide();
- },
-
- /**
- * Returns the currently visible bubble, or null if no bubble is visible.
- * @return {cr.ui.AutoCloseBubble} The bubble currently being shown.
- */
- getVisibleBubble: function() {
- var bubble = this.bubble_;
- return bubble && !bubble.hidden ? bubble : null;
- },
-
- /**
- * Callback for window.onpopstate to handle back/forward navigations.
- * @param {string} pageName The current page name.
- * @param {string} hash The hash to pass into the page.
- * @param {Object} data State data pushed into history.
- */
- setState: function(pageName, hash, data) {
- var currentOverlay = this.getVisibleOverlay_();
- var lowercaseName = pageName.toLowerCase();
- var newPage = this.registeredPages[lowercaseName] ||
- this.registeredOverlayPages[lowercaseName] ||
- this.defaultPage_;
- if (currentOverlay && !this.isAncestorOfPage(currentOverlay, newPage)) {
- currentOverlay.visible = false;
- currentOverlay.didClosePage();
- }
- this.showPageByName(pageName, false, {hash: hash});
- },
-
-
- /**
- * Whether the page is still loading (i.e. onload hasn't finished running).
- * @return {boolean} Whether the page is still loading.
- */
- isLoading: function() {
- return document.documentElement.classList.contains('loading');
- },
-
- /**
- * Callback for window.onbeforeunload. Used to notify overlays that they
- * will be closed.
- */
- willClose: function() {
- var overlay = this.getVisibleOverlay_();
- if (overlay)
- overlay.didClosePage();
- },
-
- /**
- * Freezes/unfreezes the scroll position of the root page based on the
- * current page stack.
- */
- updateRootPageFreezeState: function() {
- var topPage = this.getTopmostVisiblePage();
- if (topPage)
- this.setRootPageFrozen_(topPage.isOverlay);
- },
-
- /**
- * Change the horizontal offset used to reposition elements while showing an
- * overlay from the default.
- */
- set horizontalOffset(value) {
- this.horizontalOffset_ = value;
- },
-
- /**
- * @param {!cr.ui.pageManager.PageManager.Observer} observer The observer to
- * register.
- */
- addObserver: function(observer) {
- this.observers_.push(observer);
- },
-
- /**
- * Shows a registered overlay page. Does not update history.
- * @param {string} overlayName Page name.
- * @param {string} hash The hash state to associate with the overlay.
- * @param {cr.ui.pageManager.Page} rootPage The currently visible root-level
- * page.
- * @return {boolean} Whether we showed an overlay.
- * @private
- */
- showOverlay_: function(overlayName, hash, rootPage) {
- var overlay = this.registeredOverlayPages[overlayName.toLowerCase()];
- if (!overlay || !overlay.canShowPage())
- return false;
-
- // Save the currently focused element in the page for restoration later.
- var currentPage = this.getTopmostVisiblePage();
- if (currentPage)
- currentPage.lastFocusedElement = document.activeElement;
-
- if ((!rootPage || !rootPage.sticky) &&
- overlay.parentPage &&
- !overlay.parentPage.visible) {
- this.showPageByName(overlay.parentPage.name, false);
- }
-
- overlay.hash = hash;
- if (!overlay.visible) {
- overlay.visible = true;
- overlay.didShowPage();
- } else {
- overlay.didChangeHash();
- }
-
- // Change focus to the overlay if any other control was focused by
- // keyboard before. Otherwise, no one should have focus.
- if (document.activeElement != document.body) {
- if (FocusOutlineManager.forDocument(document).visible) {
- overlay.focus();
- } else if (!overlay.pageDiv.contains(document.activeElement)) {
- document.activeElement.blur();
- }
- }
-
- if ($('search-field') && $('search-field').value == '') {
- var section = overlay.associatedSection;
- if (section)
- options.BrowserOptions.scrollToSection(section);
- }
-
- return true;
- },
-
- /**
- * Returns whether or not an overlay is visible.
- * @return {boolean} True if an overlay is visible.
- * @private
- */
- isOverlayVisible_: function() {
- return this.getVisibleOverlay_() != null;
- },
-
- /**
- * Returns the currently visible overlay, or null if no page is visible.
- * @return {cr.ui.pageManager.Page} The visible overlay.
- * @private
- */
- getVisibleOverlay_: function() {
- var topmostPage = null;
- for (var name in this.registeredOverlayPages) {
- var page = this.registeredOverlayPages[name];
- if (!page.visible)
- continue;
-
- if (page.alwaysOnTop)
- return page;
-
- if (!topmostPage ||
- this.getNestingLevel(page) > this.getNestingLevel(topmostPage)) {
- topmostPage = page;
- }
- }
- return topmostPage;
- },
-
- /**
- * Returns the topmost visible page (overlays excluded).
- * @return {cr.ui.pageManager.Page} The topmost visible page aside from any
- * overlays.
- * @private
- */
- getTopmostVisibleNonOverlayPage_: function() {
- for (var name in this.registeredPages) {
- var page = this.registeredPages[name];
- if (page.visible)
- return page;
- }
-
- return null;
- },
-
- /**
- * Scrolls the page to the correct position (the top when opening an
- * overlay, or the old scroll position a previously hidden overlay
- * becomes visible).
- * @private
- */
- updateScrollPosition_: function() {
- var container = $('page-container');
- var scrollTop = container.oldScrollTop || 0;
- container.oldScrollTop = undefined;
- window.scroll(scrollLeftForDocument(document), scrollTop);
- },
-
- /**
- * Updates the title to the title of the current page, or of the topmost
- * visible page with a non-empty title.
- * @private
- */
- updateTitle_: function() {
- var page = this.getTopmostVisiblePage();
- while (page) {
- if (page.title) {
- for (var i = 0; i < this.observers_.length; ++i) {
- this.observers_[i].updateTitle(page.title);
- }
- return;
- }
- page = page.parentPage;
- }
- },
-
- /**
- * Constructs a new path to push onto the history stack, using observers
- * to update the history.
- * @param {boolean} replace If true, handlers should replace the current
- * history event rather than create new ones.
- * @private
- */
- updateHistoryState_: function(replace) {
- if (this.isDialog)
- return;
-
- var page = this.getTopmostVisiblePage();
- var path = window.location.pathname + window.location.hash;
- if (path) {
- // Remove trailing slash.
- path = path.slice(1).replace(/\/(?:#|$)/, '');
- }
-
- // If the page is already in history (the user may have clicked the same
- // link twice, or this is the initial load), do nothing.
- var newPath = (page == this.defaultPage_ ? '' : page.name) + page.hash;
- if (path == newPath)
- return;
-
- for (var i = 0; i < this.observers_.length; ++i) {
- this.observers_[i].updateHistory(newPath, replace);
- }
- },
-
- /**
- * Restores the last focused element on a given page.
- * @private
- */
- restoreLastFocusedElement_: function() {
- var currentPage = this.getTopmostVisiblePage();
- if (currentPage.lastFocusedElement)
- currentPage.lastFocusedElement.focus();
- },
-
- /**
- * Find an enclosing section for an element if it exists.
- * @param {Node} node Element to search.
- * @return {Node} The section element, or null.
- * @private
- */
- findSectionForNode_: function(node) {
- while (node = node.parentNode) {
- if (node.nodeName == 'SECTION')
- return node;
- }
- return null;
- },
-
- /**
- * Freezes/unfreezes the scroll position of the root page container.
- * @param {boolean} freeze Whether the page should be frozen.
- * @private
- */
- setRootPageFrozen_: function(freeze) {
- var container = $('page-container');
- if (container.classList.contains('frozen') == freeze)
- return;
-
- if (freeze) {
- // Lock the width, since auto width computation may change.
- container.style.width = window.getComputedStyle(container).width;
- container.oldScrollTop = scrollTopForDocument(document);
- container.classList.add('frozen');
- var verticalPosition =
- container.getBoundingClientRect().top - container.oldScrollTop;
- container.style.top = verticalPosition + 'px';
- this.updateFrozenElementHorizontalPosition_(container);
- } else {
- container.classList.remove('frozen');
- container.style.top = '';
- container.style.left = '';
- container.style.right = '';
- container.style.width = '';
- }
- },
-
- /**
- * Called when the page is scrolled; moves elements that are position:fixed
- * but should only behave as if they are fixed for vertical scrolling.
- * @private
- */
- handleScroll_: function() {
- this.updateAllFrozenElementPositions_();
- },
-
- /**
- * Updates all frozen pages to match the horizontal scroll position.
- * @private
- */
- updateAllFrozenElementPositions_: function() {
- var frozenElements = document.querySelectorAll('.frozen');
- for (var i = 0; i < frozenElements.length; i++)
- this.updateFrozenElementHorizontalPosition_(frozenElements[i]);
- },
-
- /**
- * Updates the given frozen element to match the horizontal scroll position.
- * @param {HTMLElement} e The frozen element to update.
- * @private
- */
- updateFrozenElementHorizontalPosition_: function(e) {
- if (isRTL()) {
- e.style.right = this.horizontalOffset + 'px';
- } else {
- var scrollLeft = scrollLeftForDocument(document);
- e.style.left = this.horizontalOffset - scrollLeft + 'px';
- }
- },
-
- /**
- * Calls the given callback with each registered page.
- * @param {boolean} includeRootPages Whether the callback should be called
- * for the root pages.
- * @param {function(cr.ui.pageManager.Page)} callback The callback.
- * @private
- */
- forEachPage_: function(includeRootPages, callback) {
- var pageNames = Object.keys(this.registeredOverlayPages);
- if (includeRootPages)
- pageNames = Object.keys(this.registeredPages).concat(pageNames);
-
- pageNames.forEach(function(name) {
- callback.call(this, this.registeredOverlayPages[name] ||
- this.registeredPages[name]);
- }, this);
- },
- };
-
- /**
- * An observer of PageManager.
- * @interface
- */
- PageManager.Observer = function() {}
-
- PageManager.Observer.prototype = {
- /**
- * Called when a page is being shown or has been hidden.
- * @param {cr.ui.pageManager.Page} page The page being shown or hidden.
- */
- onPageVisibilityChanged: function(page) {},
-
- /**
- * Called when a new title should be set.
- * @param {string} title The title to set.
- */
- updateTitle: function(title) {},
-
- /**
- * Called when a page is navigated to.
- * @param {string} path The path of the page being visited.
- * @param {boolean} replace If true, allow no history events to be created.
- */
- updateHistory: function(path, replace) {},
- };
-
- // Export
- return {
- PageManager: PageManager
- };
- });
-